<?php
/**
 * Created by PhpStorm.
 * User: ari
 * Date: 12/4/14
 * Time: 4:33 PM
 */
namespace BC\User\Session;

use BC\Config;
use BC\PGP\Commands\PGPExportPrivateKeyCommand;
use BC\PGP\PGPCommand;
use BC\SiteIndex;
use BC\User\Grant\AbstractGrant;
use BC\User\Session\Exceptions\UserSessionNotFound;
use CPath\Build\IBuildable;
use CPath\Build\IBuildRequest;
use CPath\Render\HTML\Attribute\Attributes;
use CPath\Render\HTML\Attribute\StyleAttributes;
use CPath\Render\HTML\Element\Form\HTMLForm;
use CPath\Render\HTML\Element\Form\HTMLInputField;
use CPath\Render\HTML\Element\Form\HTMLPasswordField;
use CPath\Render\HTML\Element\Form\HTMLSelectField;
use CPath\Render\HTML\Element\Form\HTMLSubmit;
use CPath\Render\HTML\Element\Form\HTMLTextAreaField;
use CPath\Render\HTML\Element\HTMLElement;
use CPath\Render\HTML\Header\HTMLHeaderScript;
use CPath\Render\HTML\HTMLContainer;
use CPath\Render\Map\MapRenderer;
use CPath\Request\Exceptions\RequestException;
use CPath\Request\Executable\ExecutableRenderer;
use CPath\Request\Executable\IExecutable;
use CPath\Request\Form\IFormRequest;
use CPath\Request\IRequest;
use CPath\Request\Session\ISessionRequest;
use CPath\Request\Validation\RequiredValidation;
use CPath\Response\Common\RedirectResponse;
use CPath\Response\IResponse;
use CPath\Response\Response;
use CPath\Route\IRoutable;
use CPath\Route\RouteBuilder;

class UserSession extends AbstractGrant implements IExecutable, IRoutable, IBuildable {
	const FORM_PATH = '/session/';
	const FORM_METHOD = 'POST';

	const PARAM_RESUME_SESSION = 'resume-session';
	const PARAM_SESSION_NAME = 'session-name';
	const PARAM_USER_FINGERPRINT = 'user-fingerprint';

	const COMMAND_LOGIN = 'login';
	const COMMAND_NEW = 'new';

	const KEYRING_NAME = '.pubring.session.gpg';

	const DEFAULT_NAME = "User Session";

	const LOG_MESSAGE      = 'msg';

	const SESSION_KEY = __CLASS__;

	const FIELD_SESSION_FINGERPRINT = 'session-fingerprint';
	const FIELD_SESSION_ID = 'session-id';

//	const FIELD_USER_FINGERPRINT = 'user-fingerprint';
//	const FIELD_USER_ID = 'user-id';

	const FIELD_PASSPHRASE = 'passphrase';

	const JSON_PASSPHRASE_COMMENTS = '{
	"#comments": [
		"/**",
		" * This is the contents of your decrypted session file.",
		" * If you are reading this, that means you successfully decrypted",
		" * your session file and authenticated your public key identity.",
		" * \'passphrase\':\'[your challenge passphrase]\'",
		" * To log in: enter the following JSON value as the challenge answer:",
		" */"]
}';

	const LOG_PATH       = 'session/%s.log';

	private static $CommandText = array(
		"Select a command" => null,
		"Start or resume session" => self::COMMAND_LOGIN,
		"Verify (tumble password)" => self::COMMAND_TUMBLE, // Session required for additional commands
		"Delete Session" => self::COMMAND_DELETE,
		"Create new Session" => self::COMMAND_NEW
	);
	/** @var \Closure */
	private $mSuccessCallback = null;

	public function __construct($sessionFingerprint) {
		parent::__construct($sessionFingerprint);
	}

	public function setSuccessCallback(\Closure $callback) {
		$this->mSuccessCallback = $callback;
		return $this;
	}

	/**
	 * Execute a command and return a response. Does not render
	 * @param IRequest $Request
	 * @throws RequestException
	 * @throws \CPath\Request\Validation\Exceptions\ValidationException
	 * @return IResponse the execution response
	 */
	function execute(IRequest $Request) {
		$requestPath = self::getRequestPath($this->getGrantFingerprint());
		$FormCommand = new HTMLForm(self::FORM_METHOD, $requestPath, static::FORM_NAME,
			new Attributes('autocomplete', 'off'),
			$InputFingerprint = new HTMLInputField(self::PARAM_FINGERPRINT, $this->getGrantFingerprint(), 'hidden'),

			new HTMLElement('fieldset', 'fieldset-wallet',
				new HTMLElement('legend', 'legend-grant', $this->getPGPInfo($Request)->getUserID()),

				/** @var HTMLSelectField $FieldCommand */
				$FieldCommand = new HTMLSelectField(self::PARAM_COMMAND, 'field-' . self::PARAM_COMMAND, self::$CommandText,
					new Attributes('onchange', 'document.location.href = "' . $Request->getDomainPath().  ltrim($requestPath, '/') . '?' . self::PARAM_COMMAND . '=" + jQuery(this).val()')
				)
//
//				"<br/>Submit command<br/>",
//				new HTMLSubmit('Submit')
			),

			new MapRenderer($this)
		);


		if(!isset($Request[self::PARAM_COMMAND]))
			return $FormCommand;

		$command = $Request[self::PARAM_COMMAND];
		$commandText = array_search($command, self::$CommandText);

		$requestPath = self::getRequestPath($this->getGrantFingerprint(), $command);
		$FormPost = new HTMLForm(self::FORM_METHOD, $requestPath, static::FORM_NAME,
			new Attributes('autocomplete', 'off'),
			new HTMLHeaderScript(AbstractGrant::DIR . '\assets\form-grant.js'),

			new HTMLInputField(self::PARAM_FINGERPRINT, $this->getGrantFingerprint(), 'hidden'),
			new HTMLInputField(self::PARAM_COMMAND, $command, 'hidden'),
//			new MapRenderer($this),

//			$CommandContainer = new HTMLElement('fieldset', 'fieldset-options',
//				new HTMLElement('legend', 'legend-grant', "Options")
//
//			),


			new HTMLElement('fieldset', 'fieldset-grant',
				new HTMLElement('legend', 'legend-grant', $commandText . ' - ' . $this->getPGPInfo($Request)->getUserID()),

				new HTMLSubmit('submit', ucwords($command))
			),

			new HTMLElement('fieldset', 'fieldset-challenge toggle',
//				new StyleAttributes('display', 'none'),


				new HTMLElement('fieldset', 'fieldset-passphrase',
					new StyleAttributes('display', 'none'),
					new HTMLElement('legend', 'legend-passphrase', "Please enter PGP Passphrase"),

					new HTMLPasswordField(self::PARAM_PASSPHRASE,
						new Attributes('disabled', 'disabled')
					)
				),

				new HTMLElement('legend', 'legend-challenge', "Grant Challenge"),

				$FieldChallenge = new HTMLTextAreaField(self::PARAM_CHALLENGE, $this->getPassphraseChallengeContent(true)),
				"<br/><br/>Grant Answer<br/>",
				$FieldAnswer = new HTMLInputField(self::PARAM_CHALLENGE_PASSPHRASE,
					new RequiredValidation()
				)
			)
		);

		switch ($command) {
			case static::COMMAND_LOGIN:
				break;
			case static::COMMAND_TUMBLE:
				break;

			case static::COMMAND_DELETE:
				break;

			default:
				throw new RequestException("Invalid UserSession Command: " . $command);
		}

		$FormPost->setFormValues($Request);

		if(!$Request instanceof IFormRequest)
			return $FormPost;

		$passphrase = $FormPost->validateField($Request, self::PARAM_CHALLENGE_PASSPHRASE);
		//$userFingerprint = $FormPost->validateField($Request, self::PARAM_USER_FINGERPRINT);
		return $this->executeGrantAction($Request, $passphrase);
//
//		return new HTMLContainer(
//			new ResponseRenderer($Response),
//			$FormPost
//		);
	}


	public function getLogPath() {
		return Config::getContentPath(sprintf(self::LOG_PATH, substr($this->getGrantFingerprint(), -16)));
	}

	public function readSessionLog($callback) {
		if(file_exists($this->getLogPath())) {
			$handle      = fopen($this->getLogPath(), 'r');
			if($handle === false) {
				touch($this->getLogPath());
				$handle      = fopen($this->getLogPath(), 'r');
				if(!$handle)
					throw new \Exception("Could not open file for reading: " . $this->getLogPath());
			}
			while ($line = fgets($handle)) {
				$pos = strpos($line, ' ');
				$cmd = substr($line, 0, $pos);
				$ret = $callback($cmd, substr($line, $pos + 1, -1));
				if($ret === true)
					break;
			}
			fclose($handle);
		}
	}

	public function writeEventToLog($eventName, $_arg=null) {
		$args = func_get_args();

		$path        = $this->getLogPath();
		$handle      = fopen($path, 'a');
		if(!$handle) {
			if(!is_dir(dirname($path)))
				mkdir(dirname($path), 0777, true);
			$handle      = fopen($path, 'a');
			if(!$handle)
				throw new \Exception("Could not write to file: " . $path);
		}
		$l = 0;
		foreach($args as $i => $arg) {
			if($i > 0) $l+=fwrite($handle, ' ');
			$l+=fwrite($handle, $arg);
		}
		fwrite($handle, "\n");
		fclose($handle);
		return $l;
	}

	/**
	 * Execute the granted action with the current passphrase
	 * @param IRequest $Request
	 * @param HTMLForm $passphrase
	 * @return \CPath\Response\IResponse
	 * @throws RequestException
	 */
	protected function executeGrantAction(IRequest $Request, $passphrase) {
		$SessionRequest = $Request;
		if(!$SessionRequest instanceof ISessionRequest)
			throw new RequestException("Request does not implement ISessionRequest");

		$command = $Request[self::PARAM_COMMAND];
		switch($command) {
			case static::COMMAND_DELETE:
				$json = $this->decryptGrantContents($Request, null);
				$this->checkJSON($json, static::JSON_PRIVATE_KEY);
				$this::delete($Request, $this, $passphrase);
				$Response = new Response("Grant deleted successfully");
				break;

			case static::COMMAND_TUMBLE:
				$this->tumblePassphrase($Request, $passphrase);
				$msg = "Session passphrase verified (tumbled) successfully";
				$Response = new RedirectResponse(self::getRequestPath($this->getGrantFingerprint()), $msg, 5);
				break;

			case static::COMMAND_LOGIN:
				if($SessionRequest->isStarted())
					$SessionRequest->endSession();
				if(!$SessionRequest->startSession())
					return new RequestException("Session could not be started: " . $this->getGrantFingerprint());

				$this->tumblePassphrase($Request, $passphrase);

				$ses = &$this->getSessionData($SessionRequest);
				$ses[self::FIELD_SESSION_FINGERPRINT] = $this->getGrantFingerprint();
				$ses[self::FIELD_SESSION_ID] = $this->getPGPInfo($Request)->getUserID();
				$ses[self::FIELD_PASSPHRASE] = $passphrase;

				$PGPExportPrivateKeyCommand = new PGPExportPrivateKeyCommand($this->getGrantFingerprint(), false);
				$PGPExportPrivateKeyCommand->setPrimaryKeyRing(static::KEYRING_NAME);
				$PGPExportPrivateKeyCommand->setArmored();
				$PGPExportPrivateKeyCommand->execute($Request);
				$exportedPrivateKey = $PGPExportPrivateKeyCommand->getExportedString();

				$challenge = $this->getPassphraseChallengeContent(true);

				$msg = "User session started successfully - " . $this->getPGPInfo($Request)->getUserID();
				$jp = self::JSON_PASSPHRASE;

				$script = <<<HTML
<textarea style="display: none;" name="session-decrypt-challenge" >{$challenge}</textarea>
<textarea style="display: none;" name="session-decrypt-key" >{$exportedPrivateKey}</textarea>
<script>
	jQuery(document).ready(function() {
		var TextAreaChallenge = jQuery('textarea[name=session-decrypt-challenge]');
		var TextAreaKey = TextAreaChallenge.next('textarea[name=session-decrypt-key]');

		TextAreaChallenge.trigger('decrypt', function(decryptedMessage) {
			var json = jQuery.parseJSON(decryptedMessage);
			var passphrase = json['{$jp}'];

			var sessionKeyring = new openpgp.Keyring.localstore('session-');
			var sessionPKs = sessionKeyring.loadPrivate();

			var keys = openpgp.key.readArmored(TextAreaKey.val());

			sessionKeyring.storePrivate(keys.keys);

		});
	});
</script>
HTML;

				$Response = new HTMLContainer(
					$script,
					new RedirectResponse(self::getRequestPath($this->getGrantFingerprint()), $msg, 5)
				);
				break;

			default:
				throw new RequestException("Invalid UserSession Command: " . $command);
		}

		if($call = $this->mSuccessCallback)
			$call($Request, $command);

		return $Response;
	}

	// Static

	/**
	 * @param $userFingerprints
	 * @param null $className
	 * @return UserSession[]
	 */
	public static function search($userFingerprints, $className=null) {
		return parent::search($userFingerprints, $className);
	}

	static function getRequestPath($sessionID=null, $command=null) {
		return self::FORM_PATH . $sessionID . ($command ? '/' . $command : '');
	}

	public static function hasSession(ISessionRequest $Request) {
		if(!$Request->hasSessionCookie())
			return false;
		$started = $Request->isStarted();
		if(!$started)
			$Request->startSession();
		$Session = &$Request->getSession();
		if(!$started)
			$Request->endSession();
		if(!isset($Session[self::SESSION_KEY]))
			return false;
		$ses = $Session[self::SESSION_KEY];
		return isset($ses[self::FIELD_SESSION_FINGERPRINT])
			&& isset($ses[self::FIELD_PASSPHRASE]);
	}

	protected static function &getSessionData(ISessionRequest $Request) {
		if(!$Request->isStarted())
			$Request->startSession();
		$Session = &$Request->getSession();
		if(!isset($Session[self::SESSION_KEY]))
			$Session[self::SESSION_KEY] = array();
		return $Session[self::SESSION_KEY];
	}

//	public static function setUserFingerprintFromSession(ISessionRequest $Request, $userFingerprint, $userID=null) {
//		$ses = &$Request->getSession(self::SESSION_KEY);
//		$ses[self::FIELD_USER_FINGERPRINT] = $userFingerprint;
//
//		if(!$userID) {
//			$User = new User($userFingerprint);
//			$PGP = $User->getPGPKey($Request);
//			$userID = $PGP->getUserID();
//		}
//		$ses[self::FIELD_USER_ID] = $userID;
//	}

//	public static function getUserFromSession(ISessionRequest $Request) {
//		$userFingerprint = self::getUserFingerprintFromSession($Request);
//		if(!$userFingerprint)
//			throw new UserSessionNotFound("User fingerprint was not found in session data");
//		if(!($userID = self::getUserIDFromSession($Request))) {
//			$User = new User($userFingerprint);
//			$PGP = $User->getPGPKey($Request);
//			$userID = $PGP->getUserID();
//			if(!$userID)
//				throw new UserSessionNotFound("User id was not found in session data");
//		}
//
//		return new User($userFingerprint, $userID);
//	}

//	public static function getUserFingerprintFromSession(ISessionRequest $Request) {
//		$ses = &self::getSessionData($Request);
//		return isset($ses[self::FIELD_USER_FINGERPRINT]) ? $ses[self::FIELD_USER_FINGERPRINT] : null;
//	}

	public static function getFingerprintFromSession(ISessionRequest $Request) {
		if(!self::hasSession($Request))
			throw new \InvalidArgumentException("User session unavailable");
		$ses = &self::getSessionData($Request);
		if(!isset($ses[self::FIELD_SESSION_FINGERPRINT]))
			throw new \InvalidArgumentException("Session fingerprint not set");
		return $ses[self::FIELD_SESSION_FINGERPRINT];
	}

	public static function getPassphraseFromSession(ISessionRequest $Request) {
		$ses = &self::getSessionData($Request);
		if(!isset($ses[self::FIELD_PASSPHRASE]))
			throw new \InvalidArgumentException("Session passphrase not set");
		return $ses[self::FIELD_PASSPHRASE];
	}

	public static function getSessionIDFromSession(ISessionRequest $Request) {
		$ses = &self::getSessionData($Request);
		if(!isset($ses[self::FIELD_SESSION_ID]))
			throw new \InvalidArgumentException("Session ID not set");
		return $ses[self::FIELD_SESSION_ID];
	}

	public static function setPassphraseFromSession(ISessionRequest $Request, $passphrase) {
		$ses = &self::getSessionData($Request);
		$ses[self::FIELD_PASSPHRASE] = $passphrase;
	}

	public static function fromSession(ISessionRequest $Request) {
		if(!$fingerprint = self::getFingerprintFromSession($Request))
			throw new UserSessionNotFound("Session fingerprint was not found in session data");
		return new UserSession($fingerprint);
	}

	public static function setPassphraseAndExecute(ISessionRequest $Request, PGPCommand $CMD) {
		$ses = &self::getSessionData($Request);
		$passphrase = $ses[self::FIELD_PASSPHRASE];

		$CMD->setPassphrase($passphrase);
		$Response = $CMD->execute($Request);
		$CMD->removeOption($CMD::OPTION_PASSPHRASE);

		return $Response;
	}

	/**
	 * @param IRequest $Request
	 * @param array $recipients
	 * @param null $grantID
	 * @param null $newPassphrase
	 * @return UserSession
	 */
	public static function create(IRequest $Request, $recipients, $grantID=null, &$newPassphrase = null) {
		/** @var UserSession $UserSession */
		$UserSession = parent::create($Request, $recipients, $grantID, $newPassphrase);
		$UserSession->writeEventToLog(UserSession::LOG_MESSAGE, "Session created - " . $UserSession->getGrantFingerprint());
		return $UserSession;
	}

	static function delete(IRequest $Request, UserSession $UserSession, $passphrase) {
		static::deleteGrant($Request, $UserSession, $passphrase);
	}

	/**
	 * Route the request to this class object and return the object
	 * @param IRequest $Request the IRequest inst for this render
	 * @param array|null $Previous all previous response object that were passed from a handler, if any
	 * @param null|mixed $_arg [varargs] passed by route map
	 * @return void|bool|Object returns a response object
	 * If nothing is returned (or bool[true]), it is assumed that rendering has occurred and the request ends
	 * If false is returned, this static handler will be called again if another handler returns an object
	 * If an object is returned, it is passed along to the next handler
	 */
	static function routeRequestStatic(IRequest $Request, Array &$Previous = array(), $_arg = null) {
		$fingerprint = null;
		if(isset($Request[self::PARAM_FINGERPRINT]))
			$fingerprint = $Request[self::PARAM_FINGERPRINT];
		if(!$fingerprint
			&& $Request instanceof ISessionRequest
			&& UserSession::hasSession($Request))
			$fingerprint = UserSession::getFingerprintFromSession($Request);
		if(!$fingerprint)
			$fingerprint = $Request[self::PARAM_FINGERPRINT];

		$UserSession = new UserSession($fingerprint);
		return new ExecutableRenderer(
			$UserSession,
			true
		);
	}

	/**
	 * Handle this request and render any content
	 * @param IBuildRequest $Request the build request inst for this build session
	 * @return void
	 * @build --disable 0
	 * Note: Use doctag 'build' with '--disable 1' to have this IBuildable class skipped during a build
	 */
	static function handleBuildStatic(IBuildRequest $Request) {
		$Builder = new RouteBuilder($Request, new SiteIndex());
		$Builder->writeRoute('ANY ' . self::FORM_PATH . ':' . self::PARAM_FINGERPRINT . '/:' . self::PARAM_COMMAND, __CLASS__);
	}
}